/*
 * Su-yun Information Technology, Co.,Ltd CONFIDENTIAL
 *
 * (C) Copyright Su-yun Information Technology, Co.,Ltd. 2021 All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains the property of
 * Su-yun Information Technology, Co.,Ltd. The intellectual property herein
 * are protected by trade secret, patent law or copyright law. Dissemination
 * of this information or reproduction of this material is strictly forbidden
 * unless prior written permission is obtained from Su-yun Information Technology.
 *
 * https://suyunplat.com/
 *
 */
package com.xtiim.battery.discharge.protocal.manager;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Maps;
import com.xtiim.battery.discharge.common.Configuration;
import com.xtiim.battery.discharge.common.Constants;
import com.xtiim.battery.discharge.common.concurrent.pool.GlobalExecutor;
import com.xtiim.battery.discharge.event.ApplicationListener;
import com.xtiim.battery.discharge.event.DefaultEventMulticaster;
import com.xtiim.battery.discharge.modules.dat.service.DataRecordService;
import com.xtiim.battery.discharge.modules.dat.service.impl.DataRecordServiceImpl;
import com.xtiim.battery.discharge.protocal.*;
import com.xtiim.battery.discharge.protocal.command.BatteryCommand;
import com.xtiim.battery.discharge.protocal.command.CommandType;
import com.xtiim.battery.discharge.protocal.event.*;
import com.xtiim.battery.discharge.util.ByteUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import purejavacomm.SerialPort;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Command manager for coordinate and schedule
 * battery inspect workflow command interaction
 * and routine data collection.
 */
@Slf4j
public class CommandManager {

    private static CommandManager instance = new CommandManager();

    private CommandManager() {
    }

    public static CommandManager getInstance() {
        return instance;
    }

    private CommandWriter commandWriter = new CommandWriter();

    private HostManager hostManager = HostManager.getInstance();

    private CommunicationManager communicationManager = CommunicationManager.getInstance();

    private DefaultEventMulticaster eventMulticaster = DefaultEventMulticaster.getInstance();

    private GlobalExecutor globalExecutor = GlobalExecutor.getInstance();

    private ScheduleDispatcher scheduleDispatcher = new ScheduleDispatcher();

    private CollectScheduler collectScheduler = new CollectScheduler();

    private CommandTimeoutEventHandler timeoutEventHandler = new CommandTimeoutEventHandler();

    private CommandFailureEventHandler failureEventHandler = new CommandFailureEventHandler();

    private PortReceiveFailureEventHandler portReceiveFailureEventHandler = new PortReceiveFailureEventHandler();

    protected ResponseReader respReader = new ResponseReader();

    private DataRecordService recordService = DataRecordServiceImpl.getInstance();

    private static int ackDelay = 10;


    public void init() {
        int ackDelayParam = Configuration.getInt(Constants.ACK_DELAY);
        if (ackDelayParam > 0) {
            ackDelay = ackDelayParam;
        }

        collectScheduler.init();
        // register event listener
        eventMulticaster.register(CommandRepliedEvent.class, scheduleDispatcher);
        eventMulticaster.register(PacketReadyEvent.class, collectScheduler);

        eventMulticaster.register(CommandTimeoutEvent.class, timeoutEventHandler);
        eventMulticaster.register(CommandFailureEvent.class, failureEventHandler);
        eventMulticaster.register(PortReceiveFailureEvent.class, portReceiveFailureEventHandler);
    }

    /**
     * Get serial port from host
     *
     * @param host
     * @return
     */
    private SerialPort getSerialPort(String host) {
        SerialPort serialPort = hostManager.getSerialPortByHost(host);
        return serialPort;
    }

    /**
     * Command timeout event handler
     */
    private class CommandTimeoutEventHandler implements ApplicationListener<CommandTimeoutEvent> {

        @Override
        public void onApplicationEvent(CommandTimeoutEvent event) {
            String port = event.getPort();
            CommandManager.this.hostManager.incrTimeoutCount(port);
        }
    }

    /**
     * Command failure event handler
     */
    private class CommandFailureEventHandler implements ApplicationListener<CommandFailureEvent> {

        @Override
        public void onApplicationEvent(CommandFailureEvent event) {
            String port = event.getPort();
            CommandManager.this.hostManager.setCommunicationFailed(port);
        }
    }

    /**
     * Port receive failure event handler
     */
    private class PortReceiveFailureEventHandler implements ApplicationListener<PortReceiveFailureEvent> {

        @Override
        public void onApplicationEvent(PortReceiveFailureEvent event) {
            String port = event.getPort();
            CommandManager.this.hostManager.setCommunicationFailed(port);
            String host = hostManager.getHostByPort(port);
            //todo: websocket
            hostManager.closeSerialPort(port);
        }
    }


    /**
     * Command schedule dispatcher.
     * <p>
     * common schedule is for interaction command like start, stop, resume
     * collect schedule is for collect data
     */
    private class ScheduleDispatcher implements ApplicationListener<CommandRepliedEvent> {

        private Map<String, CommandScheduler> schedulerMap = Maps.newConcurrentMap();

        public void schedule(String host, List<BatteryCommand> cmdList) {
            CommandScheduler scheduler = schedulerMap.computeIfAbsent(host, e -> new DefaultCommandScheduler(host));
            scheduler.init(cmdList);
            scheduler.schedule();
        }

        @Override
        public void onApplicationEvent(CommandRepliedEvent event) {
            String portName = event.getPort();
            CommandManager.this.hostManager.restoreHostCommStatus(portName);

            String host = hostManager.getHostByPort(portName);
            if (StrUtil.isBlank(host)) {
                return;
            }

            // for exclusive mode, port and host 1:1
            CommandScheduler scheduler = schedulerMap.get(host);
            if (scheduler != null) {
                scheduler.schedule();
            }
        }
    }

    /**
     * Command scheduler
     */
    private interface CommandScheduler {
        /**
         * init with command list
         *
         * @param cmdList
         */
        void init(List<BatteryCommand> cmdList);

        /**
         * schedule based on protocol
         */
        void schedule();
    }

    private abstract class AbstractCommandScheduler implements CommandScheduler {
        protected List<BatteryCommand> commandList = new CopyOnWriteArrayList<>();
        protected volatile int curCmdIndex;
        protected volatile boolean isInit;

        @Override
        public void init(List<BatteryCommand> cmdList) {
            isInit = true;
            synchronized (this) {
                commandList.clear();
                commandList.addAll(cmdList);
                isInit = false;
                curCmdIndex = 0;
            }
        }

        @Override
        public void schedule() {
            doSchedule();
        }

        abstract void doSchedule();
    }

    /**
     * Abstract scheduler
     */
    @AllArgsConstructor
    private class DefaultCommandScheduler extends AbstractCommandScheduler {

        private String host;

        @Override
        public void doSchedule() {
            synchronized (this) {
                if (curCmdIndex < commandList.size()) {
                    CommandManager.this.communicationManager.addCommand(commandList.get(curCmdIndex));
                    curCmdIndex++;
                } else {
                    // last command, indicating all finished
                    CommandMatcher commandMatcher = CommandMatcher.getInstance(host);
                    commandMatcher.reset();
                }
            }
        }
    }

    /**
     * Global collect scheduler for all hosts. Never stopping or cancel.
     * Start since the system startup. Loop all added host to send
     * collect command.
     * for single-host-single-port collection, use fixed-time-scheduling.
     */
    private class CollectScheduler implements ApplicationListener<PacketReadyEvent> {

        public void init() {
            CommandManager.this.globalExecutor.scheduleCollect(new Runnable() {
                @Override
                public void run() {
                    // for single-host-single-port collection, use fixed-time-scheduling
                    List<String> allHosts = CommandManager.this.hostManager.getAllHosts();
                    for (String host : allHosts) {
                        try {
                            byte[] bytes = CommandManager.this.commandWriter.collectRecord(Integer.valueOf(host));
                            BatteryCommand batteryCommand = new BatteryCommand(host, getSerialPort(host), CommandType.COLLECT_RECORD, bytes);
                            communicationManager.addCommand(batteryCommand);
                        } catch (Exception e) {
                            log.error("Exception occurs in collect. host: {}, error: {}", host, ExceptionUtils.getStackTrace(e));
                        }
                    }
                }
            });
        }

        @Override
        public void onApplicationEvent(PacketReadyEvent event) {
            handlePacketReadyEvent(event);
        }
    }


    /**
     * Handle packet ready event
     *
     * @param event
     */
    private void handlePacketReadyEvent(PacketReadyEvent event) {
        CommandManager.this.hostManager.restoreHostCommStatus(event.getPortName());

        // send back ack
        if (ResponseType.INSPECT_DATA.equals(respReader.responseType(event.getPacket()))) {
            ack(event.getPortName());
        }

        globalExecutor.submitHandleResponseTask(new Runnable() {
            @Override
            public void run() {
                handleResponse(event);
            }
        });
    }

    /**
     * Handle response received event
     *
     * @param event
     */
    public void handleResponse(PacketReadyEvent event) {
        String portName = event.getPortName();
        byte[] packetBytes = event.getPacket();
        log.debug("[Packet] {}", ByteUtil.byte2hex(packetBytes));
        if (!validatePacket(portName, packetBytes)) {
            return;
        }

        String host = hostManager.getHostByPort(portName);
        //clear send command num
        hostManager.clearSendCommandNum(host);

        ResponseType responseType = respReader.responseType(packetBytes);
        try {
            if (ResponseType.INSPECT_DATA.equals(responseType)) {
                // handle inspect data
                List<InspectRecord> records = respReader.readInspectRecordList(packetBytes);
                for (InspectRecord record : records) {
                    handleBatteryDataRecord(host, (InspectRecord.BatteryDataRecord) record);
                }

            } else if (ResponseType.STATUS_DATA.equals(responseType)) {
                StatusRecord statusRecord = respReader.readStatusRecord(event.getPacket());
                handleStatusRecord(host, statusRecord);
            } else {
                log.warn("Invalid collect response.");
            }
        } catch (Exception e) {
            log.error("Exception occurs in handling record.  port: {}, packet: {}, error: {}",
                    portName, ByteUtil.byte2hex(packetBytes), ExceptionUtils.getStackTrace(e));
        }
    }

    /**
     * Handle battery data record
     *
     * @param host
     * @param record
     */
    private void handleBatteryDataRecord(String host, InspectRecord.BatteryDataRecord record) {
    }


    /**
     * Handle status record
     *
     * @param host
     * @param record
     */
    private void handleStatusRecord(String host, StatusRecord record) {

        double[] voltsOfLines = record.getVoltsOfLines();
        int line = record.getLine();


    }

    private boolean validateStatusRecord(String serialPort, List<double[]> voltsOfLines) {
        if (CollectionUtil.isEmpty(voltsOfLines)) {
            log.error("status record is empty.  port: {}, voltsOfLines: {}", serialPort, voltsOfLines);
            return false;
        }

        for (int i = 0; i < voltsOfLines.size(); i++) {
            if (voltsOfLines.get(i).length != 20) {
                log.error("Invalid status record.  port: {}, voltsOfLines: {}", serialPort, voltsOfLines);
                return false;
            }
        }

        return true;
    }

    /**
     * Send back ack
     *
     * @param portName
     */
    private void ack(String portName) {
        SerialPort serialPort = CommandManager.this.hostManager.getSerialPortByPort(portName);
        String host = hostManager.getHostByPort(portName);
        log.info("ack, portName: ====> {}, host: ====> {}", portName, host);
        if (serialPort != null) {
            CommandManager.this.communicationManager.addCommand(new BatteryCommand(host, serialPort, CommandType.ACK,
                    CommandManager.this.commandWriter.ack()));
        }
    }

    /**
     * Validate packet len and xor
     *
     * @param port
     * @param bytes
     * @return
     */
    private boolean validatePacket(String port, byte[] bytes) {
        int len = Byte.toUnsignedInt(bytes[1]);
        if (bytes.length < 4 || len != bytes.length - 3) {
            log.error("[Packet validation failed] Invalid len. port: {}", port);
            return false;
        }
        byte xor = bytes[bytes.length - 1];
        byte xorVal = bytes[1];
        for (int i = 2; i < bytes.length - 2; i++) {
            xorVal ^= bytes[i];
        }
        if (xor != xorVal) {
            log.error("[Packet validation failed] Wrong xor. port: {}", port);
            return false;
        }
        return true;
    }

    /**
     * Sleep in seconds.
     *
     * @param seconds
     */
    private void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            log.error("Sleep interrupted before schedule commands.");
        }
    }

}
